Skip to content

Stabilize detach race spec by removing cross-thread detach#100

Merged
Watson1978 merged 1 commit into
socketry:mainfrom
Watson1978:fix-flaky-detach-race-spec
Jul 3, 2026
Merged

Stabilize detach race spec by removing cross-thread detach#100
Watson1978 merged 1 commit into
socketry:mainfrom
Watson1978:fix-flaky-detach-race-spec

Conversation

@Watson1978

Copy link
Copy Markdown
Collaborator

Problem

The first example in spec/detach_race_condition_spec.rb detached a watcher from a separate thread while the loop was running run_once. During the poll the GVL is released (libev is patched to do so), so that detach ended up calling ev_io_stop concurrently with the loop's backend_poll on the same ev_loop.

libev is not thread-safe, so this corrupted its internal watcher lists and crashed intermittently with a SEGV on macOS (kqueue) — reliably reproducible under load:

[BUG] Segmentation fault at 0x0000000000000000
Coolio_IOWatcher_disable+0x3c   (inside ev_io_stop)
Coolio_IOWatcher_detach

The fault is inside ev_io_stop() (wlist_del/fd_change walking anfds[fd]) racing against the loop thread's post-kevent fd_event processing.

Fix

Rewrite the example to reproduce issue #87 deterministically within a single thread: several watchers have an event pending in the same loop iteration, and the first one dispatched detaches the others. The loop must skip their now-stale pending events rather than dispatch them to a detached watcher.

This exercises the same dispatch/skip path — verified: temporarily breaking the skip logic reproduces the original TypeError: wrong argument type nil (expected Coolio::Loop) — but without the unsupported concurrent mutation of libev, and it matches how the library is actually used (a single loop per thread).

Notes / trade-off

The cross-thread detach-during-run_once scenario is genuinely unsupported: libev is not thread-safe and cool.io releases the GVL around the poll syscall, so mutating a running loop from another thread cannot be made safe without redesigning the loop's locking. That path is therefore no longer exercised by this spec.

Verification

  • Rewritten example: stable across many runs, no SEGV
  • Full spec file (both examples) × 40: 40/40 pass
  • Whole suite: 49 examples, 0 failures
  • Regression teeth confirmed: breaking the dispatch skip logic reproduces the original TypeError

🤖 Generated with Claude Code

The first example in detach_race_condition_spec.rb detached a watcher from a
separate thread while the loop was running run_once. During the poll the GVL
is released, so that detach called ev_io_stop concurrently with the loop's
backend_poll on the same ev_loop. libev is not thread-safe, so this corrupted
its internal watcher lists and crashed intermittently with a SEGV on macOS
(kqueue), reliably reproducible under load.

Rewrite the example to reproduce issue socketry#87 deterministically within a single
thread: several watchers have an event pending in the same loop iteration, and
the first one dispatched detaches the others. The loop must skip their stale
pending events rather than dispatch them to a detached watcher. This exercises
the same dispatch/skip path (verified: breaking it reproduces the original
"TypeError: wrong argument type nil") without the unsupported concurrent
mutation of libev, and matches how the library is actually used (single loop
per thread).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Watson1978 Watson1978 merged commit 41be316 into socketry:main Jul 3, 2026
14 checks passed
@Watson1978 Watson1978 deleted the fix-flaky-detach-race-spec branch July 3, 2026 08:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant